package pl.enigmatic.time;

import java.util.Comparator;

import pl.enigmatic.util.Metrics;
import pl.enigmatic.util.Time;


/**
 * Represents a time interval.
 * @author reggie_7
 * @see <a href="http://msdn.microsoft.com/en-us/library/system.timespan.aspx">msdn reference</a>
 */
public class TimeSpan implements Comparator<TimeSpan> {

	public static double TICKS_TO_SECONDS = 100 * Metrics.NANO;
	public static int SECONDS_TO_TICKS = Metrics.NANO_INV / 100;

	/**
	 * Returns a value that indicates whether two specified instances of TimeSpan are equal.
	 * @param t1 the first time interval to compare
	 * @param t2 the second time interval to compare
	 * @return true if the values of t1 and t2 are equal; otherwise, false.
	 */
	public static boolean equals(final TimeSpan t1, final TimeSpan t2) {
		if (t1 == t2) { return true; }
		if (t1 != null && t2 != null) { return t1.value == t2.value; }
		return false;
	}

	/**
	 * Returns a TimeSpan that represents a specified number of days, where the specification is accurate to the nearest millisecond.
	 * @param value a number of days, accurate to the nearest millisecond
	 * @return an object that represents value.
	 */
	public static TimeSpan fromDays(final double value) {
		return new TimeSpan(value * Time.DAYS_TO_SECONDS);
	}

	/**
	 * Returns a TimeSpan that represents a specified number of hours, where the specification is accurate to the nearest millisecond.
	 * @param value a number of hours accurate to the nearest millisecond
	 * @return an object that represents value.
	 */
	public static TimeSpan fromHours(final double value) {
		return new TimeSpan(value * Time.HOURS_TO_SECONDS);
	}

	/**
	 * Returns a TimeSpan that represents a specified number of minutes, where the specification is accurate to the nearest millisecond.
	 * @param value a number of minutes, accurate to the nearest millisecond
	 * @return an object that represents value.
	 */
	public static TimeSpan fromMinutes(final double value) {
		return new TimeSpan(value * Time.MINUTES_TO_SECONDS);
	}

	/**
	 * Returns a TimeSpan that represents a specified number of seconds, where the specification is accurate to the nearest millisecond.
	 * @param value a number of seconds, accurate to the nearest millisecond
	 * @return an object that represents value.
	 */
	public static TimeSpan fromSeconds(final double value) {
		return new TimeSpan(value);
	}

	/**
	 * Returns a TimeSpan that represents a specified number of milliseconds.
	 * @param value a number of milliseconds
	 * @return an object that represents value.
	 */
	public static TimeSpan fromMilliseconds(final double value) {
		return fromSeconds(Metrics.MILLI * value);
	}

	/**
	 * Returns a TimeSpan that represents a specified time, where the specification is in units of ticks.
	 * @param value a number of ticks that represent a time
	 * @return an object that represents value.
	 */
	public static TimeSpan fromTicks(final long value) {
		return new TimeSpan(value);
	}

	public static TimeSpan add(TimeSpan ts, double seconds) {
		return new TimeSpan(ts.value + seconds);
	}

	public static TimeSpan add(TimeSpan left, TimeSpan right) {
		return add(left, right.value);
	}

	public static TimeSpan mul(TimeSpan ts, double factor) {
		return new TimeSpan(ts.value * factor);
	}

	public static TimeSpan max(TimeSpan left, TimeSpan right) {
		return left.value > right.value ? left : right;
	}

	public static TimeSpan min(TimeSpan left, TimeSpan right) {
		return left.value < right.value ? left : right;
	}

	public static double max(TimeSpan ts, double seconds) {
		return ts.value > seconds ? ts.value : seconds;
	}

	public static double min(TimeSpan ts, double seconds) {
		return ts.value < seconds ? ts.value : seconds;
	}

	/** in seconds */
	private double value = 0;

	/**
	 * Initializes a new TimeSpan to now.
	 */
	public TimeSpan() {}

	/**
	 * Initializes a new TimeSpan to the specified time in seconds.
	 */
	public TimeSpan(final double seconds) {
		// FIXME - będą problemy z tym - przemyśleć ujemny czas, potrzeba do tego dużo przerobić ... póki co niech będzie tylko dodatni, ale i tak
		// mamy niespójność, np z funkcją negowania
		value = Math.max(0, seconds);
	}

	/**
	 * Initializes a new TimeSpan to the specified number of ticks.
	 * @param ticks a time period expressed in 100-nanosecond units
	 */
	public TimeSpan(final long ticks) {
		this(TICKS_TO_SECONDS * ticks);
	}

	/**
	 * Initializes a new TimeSpan to a specified number of days, hours, minutes, seconds, and milliseconds.
	 * @param days number of days
	 * @param hours number of hours
	 * @param minutes number of minutes
	 * @param seconds number of seconds
	 * @param milliseconds number of milliseconds
	 */
	public TimeSpan(final int days, final int hours, final int minutes, final int seconds, final int milliseconds) {
		this(Time.milliToSeconds(milliseconds) + Time.toSeconds(days, hours, minutes, seconds));
	}

	/**
	 * Initializes a new TimeSpan to a specified number of days, hours, minutes, and seconds.
	 * @param days number of days
	 * @param hours number of hours
	 * @param minutes number of minutes
	 * @param seconds number of seconds
	 */
	public TimeSpan(final int days, final int hours, final int minutes, final int seconds) {
		this(days, hours, minutes, seconds, 0);
	}

	/**
	 * Initializes a new TimeSpan to a specified number of hours, minutes, and seconds.
	 * @param hours number of hours
	 * @param minutes number of minutes
	 * @param seconds number of seconds
	 */
	public TimeSpan(final int hours, final int minutes, final int seconds) {
		this(0, hours, minutes, seconds);
	}

	/** @return the value of the current TimeSpan structure expressed in whole and fractional days */
	public double getTotalDays() {
		return value * Time.SECONDS_TO_DAYS;
	}

	/** @return the value of the current TimeSpan structure expressed in whole and fractional hours */
	public double getTotalHours() {
		return value * Time.SECONDS_TO_HOURS;
	}

	/** @return the value of the current TimeSpan structure expressed in whole and fractional minutes */
	public double getTotalMinutes() {
		return value * Time.SECONDS_TO_MINUTES;
	}

	/** @return the value of the current TimeSpan structure expressed in whole and fractional seconds */
	public double getTotalSeconds() {
		return value;
	}

	/** @return the value of the current TimeSpan structure expressed in whole and fractional milliseconds */
	public double getTotalMilliseconds() {
		return Time.secondsToMilli(value);
	}

	private static long floor(final double x) {
		return (long) (Math.floor(Math.abs(x)) * Math.signum(x));
	}

	/** @return the days component of the time interval represented by the current TimeSpan structure */
	public int getDays() {
		return (int) floor(getTotalDays());
	}

	/** @return the hours component of the time interval represented by the current TimeSpan structure */
	public int getHours() {
		return (int) floor(getTotalHours() - Time.DAYS_TO_HOURS * getDays());
	}

	/** @return the minutes component of the time interval represented by the current TimeSpan structure */
	public int getMinutes() {
		return (int) floor(getTotalMinutes() - Time.DAYS_TO_MINUTES * getDays() - Time.HOURS_TO_MINUTES * getHours());
	}

	/** @return the seconds component of the time interval represented by the current TimeSpan structure */
	public int getSeconds() {
		return (int) floor(getTotalSeconds() - Time.DAYS_TO_SECONDS * getDays() - Time.HOURS_TO_SECONDS * getHours() - Time.MINUTES_TO_SECONDS
				* getMinutes());
	}

	/** @return the milliseconds component of the time interval represented by the current TimeSpan structure */
	public long getMilliseconds() {
		return Time.secondsToMilli(value - Math.floor(value));
	}

	/** @return the number of ticks that represent the value of the current TimeSpan structure */
	public long getTicks() {
		return floor(SECONDS_TO_TICKS * value);
	}

	/**
	 * Returns a new TimeSpan object whose value is the sum of the specified TimeSpan object and this instance.
	 * @param ts the time interval to add
	 * @return a new object that represents the value of this instance plus the value of ts .
	 */
	public TimeSpan add(final TimeSpan ts) {
		return new TimeSpan(ts.value + value);
	}

	/**
	 * Compares two TimeSpan values and returns an integer that indicates whether the first value is shorter than, equal to, or longer than the second
	 * value.
	 * @param t1 the first time interval to compare
	 * @param t2 the second time interval to compare
	 * @return one of the following values. Value Description -1 t1 is shorter than t2 . 0 t1 is equal to t2 . 1 t1 is longer than t2 .
	 */
	@Override
	public int compare(final TimeSpan t1, final TimeSpan t2) {
		return Double.compare(t1.value, t2.value);
	}

	/**
	 * Compares this instance to a specified TimeSpan object and returns an integer that indicates whether this instance is shorter than, equal to, or
	 * longer than the TimeSpan object.
	 * @param value an object to compare to this instance
	 * @return a signed number indicating the relative values of this instance and value . Value Description A negative integer This instance is
	 *         shorter than value . Zero This instance is equal to value . A positive integer This instance is longer than value .
	 */
	public int compareTo(final TimeSpan value) {
		return compare(this, value);
	}

	/**
	 * Returns a new TimeSpan object whose value is the absolute value of the current TimeSpan object.
	 * @return a new object whose value is the absolute value of the current TimeSpan object.
	 */
	public TimeSpan duration() {
		return new TimeSpan(Math.abs(value));
	}

	/**
	 * Returns a value indicating whether this instance is equal to a specified TimeSpan object.
	 * @param obj an object to compare with this instance
	 * @return true if obj represents the same time interval as this instance; otherwise, false .
	 */
	public boolean equals(final TimeSpan value) {
		return equals(this, value);
	}

	/**
	 * Returns a value indicating whether this instance is equal to a specified object.
	 * @param value an object to compare with this instance
	 * @return true if value is a TimeSpan object that represents the same time interval as the current TimeSpan structure; otherwise, false .
	 */
	@Override
	public boolean equals(final Object obj) {
		if (obj instanceof TimeSpan) { return equals((TimeSpan) obj); }
		return false;
	}

	public void negate() {
		value *= -1;
	}

	public void subtract(final TimeSpan ts) {
		value -= ts.value;
	}

	public void add(final double seconds) {
		value += seconds;
	}

	public boolean before(final double seconds) {
		return value < seconds;
	}

	public boolean after(final double seconds) {
		return value > seconds;
	}

	public boolean before(final TimeSpan other) {
		return before(other.value);
	}

	public boolean after(final TimeSpan other) {
		return after(other.value);
	}

	public double fractionOf(final TimeSpan timespan) {
		return value / timespan.value;
	}

	public void set(final double seconds) {
		value = seconds;
	}

	public void set(final TimeSpan timespan) {
		set(timespan.value);
	}

	@Override
	public String toString() {
		return getClass().getSimpleName() + " [value=" + value + "]";
	}

}